/**
 * LockboxStorage
 * Manages storage and encryption of users secret information
 *
 * Created by Nathan Black
 */

#import "LockboxStorage.h"
#import "LockboxItem.h"

NSString* const NSFileIOException = @"NSFileIOException";
NSString* const LockboxInvalidKeyException = @"LockboxInvalidKeyException";
NSString* const LockboxEncryptionException = @"LockboxEncryptionException";

static LockboxStorage *s_lbStorageInstance;

@interface LockboxStorage (Private)

- (void) loadDummyData;
- (void) refreshTagCache;

@end

@implementation LockboxStorage

+ (LockboxStorage *)globalInstance
{
    if (s_lbStorageInstance == NULL) {
        s_lbStorageInstance = [[LockboxStorage alloc] init];
    }
    return s_lbStorageInstance;
}

-init 
{
    if ((self = [super init])) {
        _items = [[NSMutableArray alloc] init];
        _tagCache = [[NSMutableArray alloc] init];
        
        _filepath = @"/var/root/Library/Lockbox/Lockbox_v1.dat";
        _algorithm = @"aes128";        
    }
    return self;
}

- (void)dealloc
{
    [_key release];
    _key = NULL;

    LockboxItem* item;
    NSEnumerator *enumerator = [_items objectEnumerator];
    while ((item = [enumerator nextObject])) {
        [item release];
    }
    [_items release];
    _items = NULL;

    // We didn't retain the items in the cache
    [_tagCache release];
    _tagCache = NULL;
    
    [super dealloc];
}

- (BOOL)exists
{
    return [[NSFileManager defaultManager] fileExistsAtPath:_filepath];
}

- (BOOL)loadWithKey:(NSString*)key
{
    NSLog(@"load enter");
    _key = [key dataUsingEncoding:NSUTF8StringEncoding];
    [_key retain];

    if ([[NSFileManager defaultManager] fileExistsAtPath:_filepath]) {
        NSFileHandle* fileH;
        NSData* cipher;
        NSLog(@"file does exist");
        @try {
            fileH = [NSFileHandle fileHandleForReadingAtPath:_filepath];
            if (fileH == NULL) [NSException raise:NSFileIOException format:@"Unable to open file"];
        
            cipher = [fileH readDataToEndOfFile];
            if (cipher == NULL || [cipher length] == 0) [NSException raise:NSFileIOException format:@"readDataToEndOfFile returned null or empty data"];
            
            SSCrypto* crypto = [[SSCrypto alloc] initWithSymmetricKey:_key];
            [crypto setCipherText:cipher];
            NSData* plain = [crypto decrypt:_algorithm];
            if (plain == NULL) [NSException raise:LockboxInvalidKeyException format:@"Error with decryption using %@", _algorithm];
            
            NSMutableArray *temp = [NSKeyedUnarchiver unarchiveObjectWithData:plain];
            if (temp == NULL) [NSException raise:NSFileIOException format:@"unarchived object was null"];

            if ([temp count] > 0) {
                [_items release];
                _items = temp;
                [_items retain];
            }
            
            [self refreshTagCache];
            NSLog(@"Data read successfully");
        }
        @catch (NSException* ex) {
            NSLog(@"load catch");
            @throw;
        }
        @finally {
            [fileH closeFile];
            //[fileH release];
            //[cipher release];
        }
    }
    else {
        //[self loadDummyData];
    }
    
    [self validateData];
    NSLog(@"load exit");
}

- (void) loadDummyData
{
    LockboxItem *item = [[LockboxItem alloc] initWithType:(int)LBI_PASSWORD];
    item->key = @"Phylar";
    item->value = @"notmypassword";
    item->website = @"www.hackint0sh.org";
    [_items addObject:item];

    item = [[[LockboxItem alloc] initWithType:(int)LBI_OTHER] autorelease];
    item->key = @"SSN";
    item->value = @"123-45-6789";
    [_items addObject:item];

    item = [[[LockboxItem alloc] initWithType:(int)LBI_CARD] autorelease];
    item->key = @"DiscoverCard";
    item->value = @"1122 3344 5566 7788";
    item->expDate = @"01/10";
    item->additionalInfo = @"123";
    item->phone1 = @"18003472683";
    [_items addObject:item];
}


- (NSMutableArray *)items
{
    return _items;
}

- (NSMutableArray*)allTags
{
    return _tagCache;
}

// Remove a tag from all items
- (void)purgeTag:(NSString*)tagToDelete
{   
    NSLog(@"purgeTag: %@", tagToDelete);
    NSEnumerator* itemEnum = [_items objectEnumerator];
    LockboxItem* item;
    while ((item = [itemEnum nextObject])) {
        int i;
        for (i = 0; i < [item->tags count]; i++) {
            if ([tagToDelete caseInsensitiveCompare:[item->tags objectAtIndex:i]] == NSOrderedSame) {
                [item->tags removeObjectAtIndex:i];
                break;
            }
        }
    }
}

- (void)renameTag:(NSString*)oldTag newTag:(NSString*)newTag
{   
    NSLog(@"renameTag: %@ to %@", oldTag, newTag);
    NSEnumerator* itemEnum = [_items objectEnumerator];
    LockboxItem* item;
    while ((item = [itemEnum nextObject])) {
        int i;
        for (i = 0; i < [item->tags count]; i++) {
            if ([oldTag caseInsensitiveCompare:[item->tags objectAtIndex:i]] == NSOrderedSame) {
                [item->tags removeObjectAtIndex:i];
                [item->tags addObject:newTag];
                break;
            }
        }
    }
}

- (void)refreshTagCache
{
    [_tagCache removeAllObjects];
    
    LockboxItem* item;
    NSString* tag;
    NSString* itemTag;
    // iterate through all the item's tags
    NSEnumerator* itemEnum = [_items objectEnumerator];
    while ((item = [itemEnum nextObject])) {
        NSEnumerator* tagEnum = [item->tags objectEnumerator];
        while ((tag = [tagEnum nextObject])) {
            // check to see if we've added this tag to the cache already. If not, add it.
            BOOL bFound = false;
            NSEnumerator* itemTagEnum = [_tagCache objectEnumerator];
            while ((itemTag = [itemTagEnum nextObject])) {
                if ([itemTag compare:tag] == NSOrderedSame) {
                    bFound = true;
                break;
                }
            }
            
            if (!bFound) {
                [_tagCache addObject:tag];
            }
        }
    }
    
}

- (void)changeKey:(NSString*)newKey
{
    [_key release];
    _key = [newKey dataUsingEncoding:NSUTF8StringEncoding];
    [_key retain];
    [self save];
}

- (void)save
{
    // TODO: dirty flags would be nice to reduce writing the file all the time
    if (_key) {
        NSData* plain = [NSKeyedArchiver archivedDataWithRootObject:_items];
        
        SSCrypto* crypto = [[SSCrypto alloc] initWithSymmetricKey:_key];
        [crypto setClearTextWithData:plain];
        NSData* cipher = [crypto encrypt:_algorithm];
        if (cipher == NULL) [NSException raise:LockboxEncryptionException format:@"Error with encryption using %@", _algorithm];

        if ([[NSFileManager defaultManager] fileExistsAtPath:_filepath]) {
            if ([[NSFileManager defaultManager] fileExistsAtPath:_filepath] == NO) {
                [NSException raise:NSFileIOException format:@"Unable to delete old file before writing"];
            }
        }

        BOOL result = [[NSFileManager defaultManager] createFileAtPath:_filepath contents:cipher attributes:NULL];
        if (result == NO) [NSException raise:NSFileIOException format:@"Unable to create and write file"];
        NSLog(@"write succedded");
    }
}

- (void)validateData
{
    NSAssert(_items != NULL, @"items container must never be null");
    int i;
    for (i = 0; i < [_items count]; i++) {
        LockboxItem* item = [_items objectAtIndex:i];
        NSAssert1(item->type >= 0 && item->type < LBI_NUMTYPES, @"item type is out of range: %d", item->type);
        
        NSAssert(item->key != NULL, @"item key is null");
        NSAssert(item->value != NULL, @"item value is null");
        switch (item->type) {
            case LBI_PASSWORD:
                NSAssert(item->website != NULL, @"item website for password type is null");
                break;
            case LBI_CARD:
                NSAssert(item->expDate != NULL, @"item expDate for card type is null");
                break;
        }
    }
}

@end